#! /usr/bin/python

"""
Created on Mar 8, 2011

@author: Jonathan Herbst
"""

import signal
from threading import Thread, Event, Lock, Timer

from HardwareInterface import HardwareSimulator, DoorLatch, InitGPIOInterface, GPI, LEDs
from SoftSystem import Configuration, DoorAlert, SendC2DMMessage
from Utilities import LogFile
from PCAppServer import AppServer
from Bluetooth import Bluetooth, BluetoothInitException
import Messages

class ControlStructure():
    """
    ControlStructure
    @author: Jonathan Herbst
    
    A thread-safe priority queue for sending messages to the control system.
    """
    
    # message type values
    COMMAND = 0
    CONFIG = 1
    ALERT = 2
    EXIT = 3
    
    def __init__(self):
        """
        __init__
        @author: Jonathan Herbst
        """
        # list of command messages
        self.m_commandQueue = []
        # list of config messages
        self.m_configQueue = []
        # alert flag
        self.m_alert = False
        # exit flag
        self.m_exit = False
        
        # event triggered whenever a message is pushed or a flag is triggered
        self.m_event = Event()
        # lock for changing any of the queues
        self.m_lock = Lock()
        
        # flag to enable or disable triggering of the alert flag
        self.m_alertsEnabled = False
        
        # wait 15 sec before enabling alerts, to allow motion sensor to warm up
        Timer(15.0, self.EnableAlerts).start()
        
    def Pop(self, timeout=None):
        """
        Pop
        @author: Jonathan Herbst
        
        Pop events off the structure, blocking until an event happens or timeout
        occurs.
        
        @param timeout: number of seconds before timing out while waiting for an
            event
        
        @return: None if an event did not happen before timeout occurred
            otherwise a tuple where the first element is a list of messages of a
            certain type or "alert" in the case of an alert or "exit" in the case
            of exit, and the second element is a message type representing which
            type of messages are being returned.
        """
        retVal = None # will be changed if an event happens b4 the timeout
        # wait for an event until timeout occurs
        self.m_event.wait(timeout)
        # check if we timed out or an event occurred
        if self.m_event.isSet():    # an event occurred
            # get the lock so the state isn't changing while we are processing
            self.m_lock.acquire()
            if self.m_exit: # exit is flagged
                retVal = ("exit", self.EXIT)
            elif self.m_alert:  # alert is flagged
                # reset alert
                self.m_alert = False
                # don't allow alerts again until they are re-enabled
                self.m_alertsEnabled = False
                retVal = ("alert", self.ALERT)
            elif len(self.m_commandQueue) > 0:  # there are command messages
                retVal = (self.m_commandQueue, self.COMMAND)
                # reset the command list
                self.m_commandQueue = []
            elif len(self.m_configQueue) > 0:   # there are config messages
                retVal = (self.m_configQueue, self.CONFIG)
                # reset the config list
                self.m_configQueue = []
                
            # clear the event only if there are no other triggers
            if not self.m_exit and not self.m_alert and len(self.m_commandQueue) <= 0 and len(self.m_configQueue) <= 0:
                self.m_event.clear()
            
            # release the lock
            self.m_lock.release()
        
        return retVal
    
    def Push(self, message):
        """
        Push
        @author: Jonathan Herbst
        
        Push a message onto the corresponding list
        
        @param message: the message to push
        """
        if isinstance(message, Messages.Configuration): # a config message
            # get the lock
            self.m_lock.acquire()
            # put the message in the list
            self.m_configQueue.append(message)
            # trigger an event
            self.m_event.set()
            # release the lock
            self.m_lock.release()
        elif isinstance(message, Messages.Command): # a command message
            # get the lock
            self.m_lock.acquire()
            # put the message on the list
            self.m_commandQueue.append(message)
            # trigger an event
            self.m_event.set()
            # release the lock
            self.m_lock.release()
    
    def TriggerAlert(self):
        """
        TriggerAlert
        @author: Jonathan Herbst
        
        Trigger an alert in the control structure
        """
        # check if alerts are allowed
        if self.m_alertsEnabled:
            # get the lock
            self.m_lock.acquire()
            # set the alert flag
            self.m_alert = True
            # trigger an event
            self.m_event.set()
            # release the lock
            self.m_lock.release()
        
    def EnableAlerts(self):
        """
        EnableAlerts
        @author: Jonathan Herbst
        
        Allow alerts to be triggered
        """
        self.m_alertsEnabled = True
        
    def DisableAlerts(self):
        self.m_alertsEnabled = False
        
    def TriggerExit(self):
        """
        TriggerExit
        @author: Jonathan Herbst
        
        Trigger an exit event
        """
        # get the lock
        self.m_lock.acquire()
        # set the exit flag
        self.m_exit = True
        # trigger an event
        self.m_event.set()
        # release the lock
        self.m_lock.release()

class ControlSystem():
    """
    ControlSystem
    @author: Jonathan Herbst
    
    A system for controling the configuration and alert logic
    """
    
    # the location of the config file
    _configFileLoc = "config.ini"
    
    # hierarchy of command messages
    # command can preempt command of equal or higher level
    _commandHierarchy = {Messages.PBDoorCommand: 4,
                         Messages.PCAppDoorCommand: 3,
                         Messages.BTDoorCommand: 2,
                         Messages.SQSDoorCommand: 1,
                         Messages.DoorCommand: 0,}
    
    def __init__(self):
        """
        __init__
        @author: Jonathan Herbst
        """
        self.m_command = None
        self.m_commandTimer = None
        self.m_motion = False
        self.m_motionTimer = None
        
        # construct the configuration file
        self.m_config = Configuration(self._configFileLoc)
        
        # get the log directory
        if self.m_config.logDir is None:
            self.m_config.logDir = ""   
        # set the log level
        logLevel = LogFile.INFORMATION
        if self.m_config.debug.lower() == "yes" or self.m_config.debug.lower() == "true":
            logLevel = LogFile.DEBUG
        # construct the log file
        self.m_log = LogFile("ControlSystem", logThreshold=logLevel, logDirectory=self.m_config.logDir)
        
        # construct the Control Structure
        self.m_controlStructure = ControlStructure()
        
        # construct the hardware class
        self.m_hardware = HardwareSimulator(self.m_controlStructure)
        
        # initialize gpio
        self.m_log.Write("Startup", "Initializing GPIO", LogFile.DEBUG)
        InitGPIOInterface(self.m_config)
        
        # construct the door latch
        self.m_log.Write("Startup", "Starting Door Latch", LogFile.DEBUG)
        self.m_doorLatch = DoorLatch(self._OnStopUnlock)
        
        # construct the leds
        self.m_leds = LEDs()
        
        # holds the DoorAlert thread when there is one
        self.m_doorAlert = None
        # flag to signal exiting
        self.m_exit = False
        
        # start the PCAppServer
        self.m_log.Write("Startup", "Starting PC App Server", LogFile.DEBUG)
        self.m_appServer = AppServer(self.m_controlStructure, self.m_config)
        self.m_log.Write("Startup", "PC App Server Started", LogFile.DEBUG)
        
        # start bluetooth
        self.m_bluetooth = None
        if str(self.m_config.bluetoothEnable).lower() == 'yes':
            self.m_log.Write("Startup", "Starting Bluetooth", LogFile.DEBUG)
            try:
                self.m_bluetooth = Bluetooth(self.m_config, self.m_controlStructure)
                self.m_log.Write("Startup", "Bluetooth Startup Success", LogFile.DEBUG)
            except Exception, e:
                self.m_log.Write("Startup", "Bluetooth Startup Failed: %s" % str(e), LogFile.DEBUG)
        
        # setup gpi
        self.m_gpi = GPI()
        # bind motion sensor
        #if not self.m_gpi.Bind(self.m_config.motionSensorGPIO, self._OnSensorData):
        #    self.m_log.Write("Startup", "Failed to bind motion sensor gpio", LogFile.ERROR)
        # bind bt reset button
        #if not self.m_gpi.Bind(self.m_config.btResetGPIO, self._OnBTResetButton, 'r'):
        #    self.m_log.Write("Startup", "Failed to bind Bluetooth gpio", LogFile.ERROR)
            
        #if not self.m_gpi.Bind(self.m_config.doorBellGPIO, self._OnDoorBell, 'r'):
        #    self.m_log.Write("Startup", "Failed to bind Door Bell gpio", LogFile.ERROR)
        
    def Go(self):
        """
        Go
        @author: Jonathan Herbst
        
        Dispatch messages from the control structure until exit is flagged
        """
        self.m_log.Write("Control System", "Starting")
        # loop until exit
        while not self.m_exit:
            # pop some messages off the control structure
            messages, type = self.m_controlStructure.Pop()
            # dispatch the messages
            self.m_log.Write("Control System", "Got message", LogFile.DEBUG)
            self._DispatchMessages(messages, type)
        self.m_log.Write("Control System", "Stopping")
        # clean up all threads
        self._Stop()
        self.m_log.Write("Control System", "Stopped")
        # close the log file
        self.m_log.Close()
        
    def Shutdown(self):
        self.m_controlStructure.TriggerExit()
        
    def _Stop(self):
        """
        _Stop
        @author: Jonathan Herbst
        
        Stop all the running threads
        """
        self.m_log.Write("Stop", "Stopping")
        # stop the PCApp
        self.m_log.Write("Stop", "Stopping PC App Server", LogFile.DEBUG)
        self.m_appServer.Stop()
        self.m_log.Write("Stop", "PC App Server Stopped", LogFile.DEBUG)
        # stop bluetooth
        if self.m_bluetooth:
            self.m_log.Write("Stop", "Stopping Bluetooth", LogFile.DEBUG)
            self.m_bluetooth.Stop()
            self.m_log.Write("Stop", "Bluetooth Stopped", LogFile.DEBUG)
        # stop the door alert
        self.m_log.Write("Stop", "Stopping door alert", LogFile.DEBUG)
        self._StopDoorAlert()
        self.m_log.Write("Stop", "Door alert stopped", LogFile.DEBUG)
        # stop the door latch
        self.m_log.Write("Stop", "Stopping door latch", LogFile.DEBUG)
        self._StopDoorLatch()
        self.m_log.Write("Stop", "Door latch stopped", LogFile.DEBUG)
        
        self.m_log.Write("Stop", "Stopping control structure", LogFile.DEBUG)
        self.m_controlStructure.TriggerExit()
        self.m_log.Write("Stop", "Control structure stopped", LogFile.DEBUG)
        # stop and flush the config file
        self.m_log.Write("Stop", "Stopping config", LogFile.DEBUG)
        self.m_config.Exit()
        self.m_log.Write("Stop", "Config stopped", LogFile.DEBUG)
        
    def _StopDoorAlert(self):
        """
        _StopDoorAlert
        @author: Jonathan Herbst
        
        Stop the door alert thread if it's running
        """
        # check that there is a door alert
        if self.m_doorAlert:
            # abort the door alert
            self.m_doorAlert.Abort()
            # join the thread
            self.m_doorAlert.join()
            # reenable alerts
            #self.m_controlStructure.EnableAlerts()
        
    def _StopDoorLatch(self):
        self.m_doorLatch.Stop()
        self.m_doorLatch.join()
            
    def _DispatchMessages(self, messages, type):
        self._messageResponses[type](self, messages)
            
    def _HandleAlert(self, messages):
        self.m_log.Write("Control System", "Alert Received")
        
        self.m_leds.Yellow()
        if not self.m_doorAlert:
            self.m_log.Write("Control System", "Starting first door alert", LogFile.DEBUG)
            self.m_doorAlert = DoorAlert(self.m_config, self.m_controlStructure)
            self.m_appServer.Broadcast(Messages.EntrantDetectedLAN().renderMessage())
            return
        elif not self.m_doorAlert.IsRunning():
            self.m_log.Write("Control System", "Starting new door alert", LogFile.DEBUG)
            self.m_doorAlert = None # avoid memory leaks
            self.m_doorAlert = DoorAlert(self.m_config, self.m_controlStructure)
            self.m_appServer.Broadcast(Messages.EntrantDetectedLAN().renderMessage())
        self.m_log.Write("Control System", "door alert already running", LogFile.DEBUG)
        
    def _HandleCommand(self, messages):
        self.m_log.Write("Control System", "Command Messages Received")
        for message in reversed(messages):
            self.m_log.Write("Control System", "Command Message: %s" % str(message), LogFile.DEBUG)
            if isinstance(message, Messages.DoorCommand):
                if self._AcceptCommand(message):
                    # if it's an SQS door command, there must be an alert
                    alert = False
                    if self.m_doorAlert:
                        alert = self.m_doorAlert.IsRunning()
                    self._StopDoorAlert() # always stop the door alert if it's running
                    self._SendAlertCanceled(message.action, alert)
                    if message.action == Messages.DoorCommand.GRANTACCESSACTION:
                        self.m_leds.Green()
                        self.m_doorLatch.UnlockDoor()
                    elif message.action == Messages.DoorCommand.DENYACCESSACTION:
                        self.m_leds.Red()
                        self.m_doorLatch.LockDoor()
                else:
                    self.m_log.Write("Control System", "Command Rejected")
                    
    def _SendAlertCanceled(self, action, alert):
        class AlertCanceledThread(Thread):
            def __init__(self, pcApp, config, action, alert):
                self.m_pcApp = pcApp
                self.m_config = config
                self.m_action = action
                self.m_alert = alert
                
                Thread.__init__(self)
                self.start()
            
            def run(self):
                pcAppMessage = None
                message = None
                if self.m_action == Messages.DoorCommand.GRANTACCESSACTION:
                    pcAppMessage = Messages.AlertCanceled(Messages.DoorCommand.GRANTACCESSACTION).renderMessage()
                    if self.m_alert:
                        message = Messages.AlertCanceled(Messages.DoorCommand.GRANTACCESSACTION).renderMessage()
                    else:
                        message = Messages.CommandReceived(Messages.DoorCommand.GRANTACCESSACTION).renderMessage()
                    
                elif self.m_action == Messages.DoorCommand.DENYACCESSACTION:
                    pcAppMessage = Messages.AlertCanceled(Messages.DoorCommand.DENYACCESSACTION).renderMessage()
                    if self.m_alert:
                        message = Messages.AlertCanceled(Messages.DoorCommand.DENYACCESSACTION).renderMessage()
                    else:
                        message = Messages.CommandReceived(Messages.DoorCommand.DENYACCESSACTION).renderMessage()
                    
                if message:      
                    try:
                        self.m_pcApp.Broadcast(pcAppMessage)
                    except Exception, e:
                        self.m_log.Write("Alert Canceled", "Failed PCApp Send: %s" % str(e))
                    try:
                        
                        SendC2DMMessage(self.m_config, message)
                    except Exception, e:
                        self.m_log.Write("Alert Canceled", "Failed C2DM Send: %s" % str(e))
        
        AlertCanceledThread(self.m_appServer, self.m_config, action, alert)
            
    def _HandleConfiguration(self, messages):
        self.m_log.Write("Control System", "Configuration Message Received")
            
    def _Exit(self, event):
        self.m_log.Write("Control System", "Exit signal received")
        self.m_exit = True
    
    def _AcceptCommand(self, command):
        if command.__class__ not in self._commandHierarchy:
            return False
        
        currentCommandLevel = 0
        if self.m_command:
            currentCommandLevel = self._commandHierarchy[self.m_command.__class__]
        
        nextCommandLevel = self._commandHierarchy[command.__class__]
        if nextCommandLevel < currentCommandLevel:
            return False
        
        # command accepted
        if self.m_commandTimer:
            # cancel and join to let the timer finish if it gets to the callback
            self.m_commandTimer.cancel()
            self.m_commandTimer.join()
            
        self.m_controlStructure.DisableAlerts()
            
        self.m_command = command
        self.m_commandTimer = Timer(30.0, self._OnCommandTimerFinish)
        self.m_commandTimer.start()
        return True
        
    def _OnCommandTimerFinish(self):
        self.m_command = None
        self.m_controlStructure.EnableAlerts()
    
    def _OnSensorData(self, gpioInfo):
        if str(gpioInfo[1]).lower() == 'f': # falling edge (motion detected)
            self.m_controlStructure.TriggerAlert()
            self.m_motion = True
            if self.m_motionTimer:
                if self.m_motionTimer.isAlive():
                    self.m_motionTimer.cancel()
                    self.m_motionTimer.join()
            self.m_leds.MotionLED()
            self.m_motionTimer = Timer(30.0, self._OnMotionTimerFinish)
            self.m_motionTimer.start()
        else: # rising edge (motion stopped)
            self.m_motion = False
            
        
    def _OnMotionTimerFinish(self):
        if not self.m_motion: # motion has stopped
            self.m_leds.MotionLED(False)
        else:
            # keep the timer going if motion is still detected
            self.m_motionTimer = Timer(15.0, self._OnMotionTimerFinish)
        
    def _OnDoorBell(self, gpioInfo):
        print "Door Bell"
        self.m_controlStructure.TriggerAlert()
    
    def _OnBTResetButton(self, gpioInfo):
        pass
    
    def _OnStopUnlock(self):
        self.m_leds.Red()
        
    _messageResponses = {ControlStructure.EXIT: _Exit,
                        ControlStructure.COMMAND: _HandleCommand,
                        ControlStructure.CONFIG: _HandleConfiguration,
                        ControlStructure.ALERT: _HandleAlert
                        }
    
controlSystem = None

def handleSigTerm(signum, frame):
    if controlSystem:
        controlSystem.m_controlStructure.TriggerExit()
        
if __name__ == "__main__":
    controlSystem = ControlSystem()
    signal.signal(signal.SIGTERM, handleSigTerm)
    try:
        controlSystem.Go()
    except KeyboardInterrupt:
        controlSystem.Shutdown()
        